// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef MEDIA_MIDI_MIDI_MANAGER_ALSA_H_
#define MEDIA_MIDI_MIDI_MANAGER_ALSA_H_

#include <alsa/asoundlib.h>
#include <stdint.h>

#include <map>
#include <memory>
#include <utility>
#include <vector>

#include "base/containers/hash_tables.h"
#include "base/gtest_prod_util.h"
#include "base/macros.h"
#include "base/synchronization/lock.h"
#include "base/threading/thread.h"
#include "base/values.h"
#include "device/udev_linux/scoped_udev.h"
#include "media/midi/midi_export.h"
#include "media/midi/midi_manager.h"

namespace base {
class ThreadChecker;
}

namespace midi {

class MIDI_EXPORT MidiManagerAlsa final : public MidiManager {
public:
    MidiManagerAlsa();
    ~MidiManagerAlsa() override;

    // MidiManager implementation.
    void StartInitialization() override;
    void Finalize() override;
    void DispatchSendMidiData(MidiManagerClient* client,
        uint32_t port_index,
        const std::vector<uint8_t>& data,
        double timestamp) override;

private:
    friend class MidiManagerAlsaTest;
    FRIEND_TEST_ALL_PREFIXES(MidiManagerAlsaTest, ExtractManufacturer);
    FRIEND_TEST_ALL_PREFIXES(MidiManagerAlsaTest, ToMidiPortState);

    class AlsaCard;
    using AlsaCardMap = std::map<int, std::unique_ptr<AlsaCard>>;

    class MidiPort {
    public:
        enum class Type { kInput,
            kOutput };

        // The Id class is used to keep the multiple strings separate
        // but compare them all together for equality purposes.
        // The individual strings that make up the Id can theoretically contain
        // arbitrary characters, so unfortunately there is no simple way to
        // concatenate them into a single string.
        class Id final {
        public:
            Id();
            Id(const std::string& bus,
                const std::string& vendor_id,
                const std::string& model_id,
                const std::string& usb_interface_num,
                const std::string& serial);
            Id(const Id&);
            ~Id();
            bool operator==(const Id&) const;
            bool empty() const;

            std::string bus() const { return bus_; }
            std::string vendor_id() const { return vendor_id_; }
            std::string model_id() const { return model_id_; }
            std::string usb_interface_num() const { return usb_interface_num_; }
            std::string serial() const { return serial_; }

        private:
            std::string bus_;
            std::string vendor_id_;
            std::string model_id_;
            std::string usb_interface_num_;
            std::string serial_;
        };

        MidiPort(const std::string& path,
            const Id& id,
            int client_id,
            int port_id,
            int midi_device,
            const std::string& client_name,
            const std::string& port_name,
            const std::string& manufacturer,
            const std::string& version,
            Type type);
        ~MidiPort();

        // Gets a Value representation of this object, suitable for serialization.
        std::unique_ptr<base::Value> Value() const;

        // Gets a string version of Value in JSON format.
        std::string JSONValue() const;

        // Gets an opaque identifier for this object, suitable for using as the id
        // field in MidiPort.id on the web. Note that this string does not store
        // the full state.
        std::string OpaqueKey() const;

        // Checks for equality for connected ports.
        bool MatchConnected(const MidiPort& query) const;
        // Checks for equality for kernel cards with id, pass 1.
        bool MatchCardPass1(const MidiPort& query) const;
        // Checks for equality for kernel cards with id, pass 2.
        bool MatchCardPass2(const MidiPort& query) const;
        // Checks for equality for non-card clients, pass 1.
        bool MatchNoCardPass1(const MidiPort& query) const;
        // Checks for equality for non-card clients, pass 2.
        bool MatchNoCardPass2(const MidiPort& query) const;

        // accessors
        std::string path() const { return path_; }
        Id id() const { return id_; }
        std::string client_name() const { return client_name_; }
        std::string port_name() const { return port_name_; }
        std::string manufacturer() const { return manufacturer_; }
        std::string version() const { return version_; }
        int client_id() const { return client_id_; }
        int port_id() const { return port_id_; }
        int midi_device() const { return midi_device_; }
        Type type() const { return type_; }
        uint32_t web_port_index() const { return web_port_index_; }
        bool connected() const { return connected_; }

        // mutators
        void set_web_port_index(uint32_t web_port_index)
        {
            web_port_index_ = web_port_index;
        }
        void set_connected(bool connected) { connected_ = connected; }
        void Update(const std::string& path,
            int client_id,
            int port_id,
            const std::string& client_name,
            const std::string& port_name,
            const std::string& manufacturer,
            const std::string& version)
        {
            path_ = path;
            client_id_ = client_id;
            port_id_ = port_id;
            client_name_ = client_name;
            port_name_ = port_name;
            manufacturer_ = manufacturer;
            version_ = version;
        }

    private:
        // Immutable properties.
        const Id id_;
        const int midi_device_;

        const Type type_;

        // Mutable properties. These will get updated as ports move around or
        // drivers change.
        std::string path_;
        int client_id_;
        int port_id_;
        std::string client_name_;
        std::string port_name_;
        std::string manufacturer_;
        std::string version_;

        // Index for MidiManager.
        uint32_t web_port_index_ = 0;

        // Port is present in the ALSA system.
        bool connected_ = true;

        DISALLOW_COPY_AND_ASSIGN(MidiPort);
    };

    class MidiPortStateBase {
    public:
        typedef std::vector<std::unique_ptr<MidiPort>>::iterator iterator;

        virtual ~MidiPortStateBase();

        // Given a port, finds a port in the internal store.
        iterator Find(const MidiPort& port);

        // Given a port, finds a connected port, using exact matching.
        iterator FindConnected(const MidiPort& port);

        // Given a port, finds a disconnected port, using heuristic matching.
        iterator FindDisconnected(const MidiPort& port);

        iterator begin() { return ports_.begin(); }
        iterator end() { return ports_.end(); }

    protected:
        MidiPortStateBase();
        iterator erase(iterator position) { return ports_.erase(position); }
        void push_back(std::unique_ptr<MidiPort> port)
        {
            ports_.push_back(std::move(port));
        }

    private:
        std::vector<std::unique_ptr<MidiPort>> ports_;

        DISALLOW_COPY_AND_ASSIGN(MidiPortStateBase);
    };

    class TemporaryMidiPortState final : public MidiPortStateBase {
    public:
        iterator erase(iterator position)
        {
            return MidiPortStateBase::erase(position);
        };
        void push_back(std::unique_ptr<MidiPort> port)
        {
            MidiPortStateBase::push_back(std::move(port));
        }
    };

    class MidiPortState final : public MidiPortStateBase {
    public:
        MidiPortState();

        // Inserts a port at the end. Returns web_port_index.
        uint32_t push_back(std::unique_ptr<MidiPort> port);

    private:
        uint32_t num_input_ports_ = 0;
        uint32_t num_output_ports_ = 0;
    };

    class AlsaSeqState {
    public:
        enum class PortDirection { kInput,
            kOutput,
            kDuplex };

        AlsaSeqState();
        ~AlsaSeqState();

        void ClientStart(int client_id,
            const std::string& client_name,
            snd_seq_client_type_t type);
        bool ClientStarted(int client_id);
        void ClientExit(int client_id);
        void PortStart(int client_id,
            int port_id,
            const std::string& port_name,
            PortDirection direction,
            bool midi);
        void PortExit(int client_id, int port_id);
        snd_seq_client_type_t ClientType(int client_id) const;
        std::unique_ptr<TemporaryMidiPortState> ToMidiPortState(
            const AlsaCardMap& alsa_cards);

        int card_client_count() { return card_client_count_; }

    private:
        class Port {
        public:
            Port(const std::string& name, PortDirection direction, bool midi);
            ~Port();

            std::string name() const { return name_; }
            PortDirection direction() const { return direction_; }
            // True if this port is a MIDI port, instead of another kind of ALSA port.
            bool midi() const { return midi_; }

        private:
            const std::string name_;
            const PortDirection direction_;
            const bool midi_;

            DISALLOW_COPY_AND_ASSIGN(Port);
        };

        class Client {
        public:
            using PortMap = std::map<int, std::unique_ptr<Port>>;

            Client(const std::string& name, snd_seq_client_type_t type);
            ~Client();

            std::string name() const { return name_; }
            snd_seq_client_type_t type() const { return type_; }
            void AddPort(int addr, std::unique_ptr<Port> port);
            void RemovePort(int addr);
            PortMap::const_iterator begin() const;
            PortMap::const_iterator end() const;

        private:
            const std::string name_;
            const snd_seq_client_type_t type_;
            PortMap ports_;

            DISALLOW_COPY_AND_ASSIGN(Client);
        };

        std::map<int, std::unique_ptr<Client>> clients_;

        // This is the current number of clients we know about that have
        // cards. When this number matches alsa_card_midi_count_, we know
        // we are in sync between ALSA and udev. Until then, we cannot generate
        // MIDIConnectionEvents to web clients.
        int card_client_count_ = 0;

        DISALLOW_COPY_AND_ASSIGN(AlsaSeqState);
    };

    class AlsaCard {
    public:
        AlsaCard(udev_device* dev,
            const std::string& name,
            const std::string& longname,
            const std::string& driver,
            int midi_device_count);
        ~AlsaCard();
        std::string name() const { return name_; }
        std::string longname() const { return longname_; }
        std::string driver() const { return driver_; }
        std::string path() const { return path_; }
        std::string bus() const { return bus_; }
        std::string vendor_id() const { return vendor_id_; }
        std::string model_id() const { return model_id_; }
        std::string usb_interface_num() const { return usb_interface_num_; }
        std::string serial() const { return serial_; }
        int midi_device_count() const { return midi_device_count_; }
        std::string manufacturer() const { return manufacturer_; }

    private:
        FRIEND_TEST_ALL_PREFIXES(MidiManagerAlsaTest, ExtractManufacturer);

        // Extracts the manufacturer using heuristics and a variety of sources.
        static std::string ExtractManufacturerString(
            const std::string& udev_id_vendor,
            const std::string& udev_id_vendor_id,
            const std::string& udev_id_vendor_from_database,
            const std::string& name,
            const std::string& longname);

        const std::string name_;
        const std::string longname_;
        const std::string driver_;
        const std::string path_;
        const std::string bus_;
        const std::string vendor_id_;
        const std::string model_id_;
        const std::string usb_interface_num_;
        const std::string serial_;
        const int midi_device_count_;
        const std::string manufacturer_;

        DISALLOW_COPY_AND_ASSIGN(AlsaCard);
    };

    struct SndSeqDeleter {
        void operator()(snd_seq_t* seq) const { snd_seq_close(seq); }
    };

    struct SndMidiEventDeleter {
        void operator()(snd_midi_event_t* coder) const
        {
            snd_midi_event_free(coder);
        };
    };

    using SourceMap = base::hash_map<int, uint32_t>;
    using OutPortMap = base::hash_map<uint32_t, int>;
    using ScopedSndSeqPtr = std::unique_ptr<snd_seq_t, SndSeqDeleter>;
    using ScopedSndMidiEventPtr = std::unique_ptr<snd_midi_event_t, SndMidiEventDeleter>;

    // An internal callback that runs on MidiSendThread.
    void SendMidiData(uint32_t port_index, const std::vector<uint8_t>& data);

    void ScheduleEventLoop();
    void EventLoop();
    void ProcessSingleEvent(snd_seq_event_t* event, double timestamp);
    void ProcessClientStartEvent(int client_id);
    void ProcessPortStartEvent(const snd_seq_addr_t& addr);
    void ProcessClientExitEvent(const snd_seq_addr_t& addr);
    void ProcessPortExitEvent(const snd_seq_addr_t& addr);
    void ProcessUdevEvent(udev_device* dev);
    void AddCard(udev_device* dev);
    void RemoveCard(int number);

    // Updates port_state_ and Web MIDI state from alsa_seq_state_.
    void UpdatePortStateAndGenerateEvents();

    // Enumerates ports. Call once after subscribing to the announce port.
    void EnumerateAlsaPorts();
    // Enumerates udev cards. Call once after initializing the udev monitor.
    bool EnumerateUdevCards();
    // Returns true if successful.
    bool CreateAlsaOutputPort(uint32_t port_index, int client_id, int port_id);
    void DeleteAlsaOutputPort(uint32_t port_index);
    // Returns true if successful.
    bool Subscribe(uint32_t port_index, int client_id, int port_id);

    // Members initialized in the constructor are below.
    // Our copies of the internal state of the ports of seq and udev.
    AlsaSeqState alsa_seq_state_;
    MidiPortState port_state_;

    // One input port, many output ports.
    base::Lock out_ports_lock_; // guards out_ports_
    OutPortMap out_ports_; // guarded by out_ports_lock_

    // Mapping from ALSA client:port to our index.
    SourceMap source_map_;

    // Mapping from card to devices.
    AlsaCardMap alsa_cards_;

    // This is the current count of midi devices across all cards we know
    // about. When this number matches card_client_count_ in AlsaSeqState,
    // we are safe to generate MIDIConnectionEvents. Otherwise we need to
    // wait for our information from ALSA and udev to get back in sync.
    int alsa_card_midi_count_ = 0;

    base::Lock shutdown_lock_; // guards event_thread_shutdown_
    bool event_thread_shutdown_ = false; // guarded by shutdown_lock_

    // This lock is needed to ensure that members destroyed in Finalize
    // will be visibly destroyed before the destructor is run in the
    // other thread. Otherwise, the same objects may have their destructors
    // run multiple times in different threads.
    base::Lock lazy_init_member_lock_; // guards members below

    // Members initialized in StartInitialization() are below.
    // Make sure to destroy these in Finalize()!
    std::unique_ptr<base::ThreadChecker> initialization_thread_checker_;

    // ALSA seq handles and ids.
    ScopedSndSeqPtr in_client_;
    int in_client_id_;
    ScopedSndSeqPtr out_client_;
    int out_client_id_;
    int in_port_id_;

    // ALSA event -> MIDI coder.
    ScopedSndMidiEventPtr decoder_;

    // udev, for querying hardware devices.
    device::ScopedUdevPtr udev_;
    device::ScopedUdevMonitorPtr udev_monitor_;

    // Threads for sending and receiving. These are initialized in the
    // constructor, but are started at the end of StartInitialization.
    base::Thread event_thread_;
    base::Thread send_thread_;

    DISALLOW_COPY_AND_ASSIGN(MidiManagerAlsa);
};

} // namespace midi

#endif // MEDIA_MIDI_MIDI_MANAGER_ALSA_H_
